Skip to content

fix(nextjs-mf): await async factories and return wrapper factory in server onLoad - #4974

Open
shashank-u03 wants to merge 2 commits into
module-federation:mainfrom
shashank-u03:fix/nextjs-mf-onload-async-fact
Open

fix(nextjs-mf): await async factories and return wrapper factory in server onLoad#4974
shashank-u03 wants to merge 2 commits into
module-federation:mainfrom
shashank-u03:fix/nextjs-mf-onload-async-fact

Conversation

@shashank-u03

Copy link
Copy Markdown
Contributor

Description

Remote containers in @module-federation/runtime expose async module factories (RemoteEntryExports.get returns () => Promise<Module>).

During the webpack build/SSR path, runtime-core loads remotes with loadFactory: false and from: 'build' (see packages/webpack-bundler-runtime/src/remotes.ts), passing the unexecuted factory to nextjs-mf's onLoad hook as exposeModuleFactory.

On the server, onLoad synchronously invoked that factory and Proxy-wrapped the return value for chunk-usage tracking. When the factory is async, the sync invocation returned a raw Promise. Proxy-wrapping that Promise caused webpack's async module runtime to fail when calling .then():

TypeError: Method Promise.prototype.then called on incompatible receiver [object Promise]

runtime-core catches this in loadRemote and routes it to errorLoadRemote with lifecycle: 'onLoad' and from: 'build'.

Fix:

  1. Await Promise results from moduleOrFactory() before applying the existing Proxy wrapper on the server.
  2. When the resolved export is a namespace object and the input was exposeModuleFactory, return a wrapper factory so loadRemote can adopt the proxied result (runtime-core only uses onLoad's return when it is a function - see packages/runtime-core/src/remote/index.ts).

runtime-core already handles async factories in module.wraperFactory; this aligns nextjs-mf onLoad with that behavior.

Tests added in runtimePlugin.test.ts:

  • Async factory is awaited before proxy-wrapping on the server
  • Wrapper factory is returned for async namespace exports on the server
  • Promise.prototype.then is not broken after the fix
  • Sync exposeModuleFactory still works
  • Rejected async factories propagate correctly
  • Client path (window present) still returns args unchanged
    Note on async onLoad: loadRemote always awaits onLoad.emit() and AsyncHook.emit wraps listener returns with Promise.resolve(). The client branch still returns args unchanged; only the server branch gained internal await logic.

Related Issue

#2218

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist

  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have updated the documentation.

@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 57cacb9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@module-federation/nextjs-mf Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@shashank-u03 shashank-u03 changed the title fix(nextjs-mf): await async factories in server onLoad hook fix(nextjs-mf): await async factories and return wrapper factory in server onLoad Aug 8, 2026
@shashank-u03
shashank-u03 force-pushed the fix/nextjs-mf-onload-async-fact branch from eb80f2d to 194e558 Compare August 8, 2026 18:23
@shashank-u03

Copy link
Copy Markdown
Contributor Author

Hey @ScriptedAlchemy @2heal1 - this fixes the Promise.prototype.then crash from #2218 (root cause + fix details in the description above). Async factories were getting Proxy-wrapped before being awaited, which shadowed .then() and broke webpack's async module runtime. Added tests for the async/sync paths and rejection handling. Would appreciate a review when you get a chance!

if (exposeModuleFactory) {
const wrappedExports = exposedModuleExports;
return function () {
return wrappedExports;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning wrappedExports as the module factory makes the namespace proxy observable to webpack. The proxy's get trap currently replaces every function export with a plain wrapper that
invokes originalMethod.apply(...), which does not preserve constructor/class semantics.

For example, an async factory resolving to:

{ default: class RemoteComponent {} }

now produces an export where new exports.default() throws:

TypeError: Class constructor RemoteComponent cannot be invoked without 'new'

This can break default-exported React class components and other constructible exports during SSR/build. Could we preserve both call and construct behavior, for example by proxying exported
functions with apply and construct traps using Reflect.apply/Reflect.construct? Please also add a regression test verifying that a class returned by an async factory remains
constructible and preserves instanceof.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e7e1e0f replaced the .apply()-based wrapper with wrapCallableForChunkTracking, a Proxy using apply/construct traps (Reflect.apply/Reflect.construct), so constructible exports work through the proxy on the server.

Added the regression test you asked for: keeps class default export constructible after async factory, asserting new exports.default() + instanceof both hold. Also covered usedChunks tracking on construction and static-prop/plain-function cases.

Note: static methods on the wrapped default (exports.default.someStatic()) aren't individually tracked anymore, only top-level apply/construct marks usedChunks. Since tracking is at remote/expose granularity, not per-method, this shouldn't matter in practice, but flag if you'd want it covered explicitly.

@shashank-u03
shashank-u03 force-pushed the fix/nextjs-mf-onload-async-fact branch from 8c57991 to e7e1e0f Compare August 18, 2026 10:20
@shashank-u03
shashank-u03 requested a review from 2heal1 August 18, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants